/*---------------------------------------------------------------------------
BoxViewSwitcher.m -- switch between box views using a popup list (button)

Copyright (c) 1990 Doug Brenner

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 1, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA, or send
   electronic mail to the the author.

Implementation for the BoxViewSwitcher class.  BoxViewSwitcher allows you to
switch between a group of views using a popup button.  (Something like the
popup button on the Inspector panel of Interface Builder.)

*** Using BoxViewSwitcher

This is the easy part because you do everything in Interface Builder!  Here
is a step-by-step guide meant for people familiar with Interface Builder.

1. Setup: Launch IB, create a new application, create a project, and save.

2. Main window setup: You should already have one window from when you
created the new application.  This will be the main window with which people
will interacte.  On this window create one button and one box view.  Within
the box view put any controls that you want and give the box view a name.

This box view will be the "default" box view that people will see when your
application is launched.

3. Alternate window setup: Create another window.  (I'll refer to this window
as the "alternate" window and the window from step 2 as the "main" window.)
Select and copy the box view from the main window and paste it onto this
alternate window.  Give this newly pasted box view a different name and
update it's controls, etc., to your needs.

If you want more than two alternate views, repeat the copy/paste/udpate
process until you have all you need.  BoxViewSwitcher will locate all the box
views at the "top level" of the alternate window.  (In other words, it
doesn't descend the view hierarchy looking for box views within views.)

4. Bring in BoxViewSwitcher: Open the Classes panel and create a subclass of
Object.  (Make sure it's a subclass of object.)  Rename this subclass as
BoxViewSwitcher.  Drag copies of BoxViewSwitcher.h and BoxViewSwitcher.m
into the same folder as this Interface Builder project.

Choose Parse from the Classes popdown button. (Click "Replace" since you know
the outlets, etc., are different.)

Choose Instantiate from the Classes popdown button.  You should see a new
instance (probably named BoxViewSwitcherInstance) in the objects window.

Add BoxViewSwitcher.[hm] to the Files section of the Project window.

5. Attach the BoxViewSwitcher instance outlets: There are only three outlets
that you need connect: controlButton, boxView, and boxViewWindow.  (Ignore
boxViewList and popup.)

Connect the BoxViewSwitcher instance outlets as follows:

   Outlet Name     Connect to:
   -------------   -------------------------------------------------
   controlButton   button on the main window
   boxView         box view on the main window
   boxViewWindow   alternate window of box views (the entire window)

(If you want the instantiated BoxViewSwitcher to send delegate methods [see
setDelegate:], connect some other custom object to the delegate outlet.)

6. Make everything: Save and then make the application.

*** Hints

You can connect the controls in any of the box views to your own custom
objects or other controls.  These connections will be maintained even after
the box views "move" from one window to the other.

In general you want all the box views to be the same size and to have unique
names.  The alternate box view window should have "Visible at Launch Time"
(in the Inspector panel) unchecked.

You can also create menu items to select between the various box views.  Just
name the menu item the same as the desired box view title and connect the
menu item to BoxViewSwitcher's selectBoxView: method.

All three outlets (controlButton, boxView, boxViewWindow) must be connected,
and connected to the proper classes or nothing will happen.

BoxViewSwitcher assumes the alternate window is for its use only.  After it
collects all the box views, the alternate window is freed.  (I.e., if you
need a place for other things, don't put them on the alternate window.)

*** Other controls and the selectBoxView: method

BoxViewSwitcher only knows about one control, controlButton.  This means only
controlButton is always updated to reflect which box view is currently
displayed.  Why is this important?

Let's say you attach a radio button matrix to the selectBoxView: method;
things will look fine at first.  The matrix will correctly select between the
various box views based on which button is selected.  The problem shows up
when you use the controlButton/popup combination to select a box view.

Because BoxViewSwitcher doesn't know about your radio button matrix, it
isn't updated after a new selection is made.  That is the problem.

To deal with this situation, the delegate method boxViewDidSwitch:to: has
been supplied.  Use it to update other controls you might use to select
between box views.

To solve the radio button problem given above, you might try something like
this in your delegate:

   #import <appkit/Button.h>
   #import <appkit/Matrix.h>
   #import <appkit/Box.h>
   #import <objc/List.h>

   - boxViewDidSwitch:sender
   {
      id cells, cell;
      int i;
      const char *newTitle;

      cells = [radioButtonMatrix cellList];
      for (i = [cells count]-1; i >= 0; i--) {
         cell = [cells objectAt:i];
         newTitle = [[sender boxView] title];
         if (strcmp ([cell title], newTitle) == 0) {
            [radioButtonMatrix selectCell:cell];
            break;
         }
      }
      return self;
   }
   
   return self;

And then again, you may find the above code silly.  Use it as you see fit.

Doug Brenner <dbrenner@umaxc.weeg.uiowa.edu>

$Header: /rpruess/apps/Remotes3.0/RCS/BoxViewSwitcher.m,v 3.0 92/09/23 22:16:54 rpruess Exp $
-----------------------------------------------------------------------------
$Log:	BoxViewSwitcher.m,v $
Revision 3.0  92/09/23  22:16:54  rpruess
Checked in to RCS to get the revision number updated to 3.0.

Revision 2.1  92/09/23  21:29:31  rpruess
Updated code for NeXT System Release 3.0.

Revision 2.0  91/01/22  15:24:01  rpruess
Incorporated into Remotes at Release 2.0.

Revision 2.0  91/01/22  13:13:36  rpruess
Initial production release of Remotes-2.0.

Revision 2.0  90/06/22  18:28:39  dbrenner
Comment changes; added delegate notification in selectBoxViewTitle: (this
required a new instance variable, delegate, and method (setDelegate:);
added methods to return most instance variables (boxViewList, boxView,
popup, controlButton); moved code to force the controlButton title from
replaceBoxViewWith: to selectBoxViewTitle: (this seems more reasonable);
added archive methods (read and write); added class method initialize
to set the class version number.

Revision 1.3  90/06/05  17:06:08  dbrenner
Minor comment changes for automatic documentation generation.

Revision 1.2  90/05/26  14:52:16  dbrenner
Many more comments; added code to update the controlButton title after
a switch has been made (in case things were done via another control).

Revision 1.1  90/05/18  21:12:44  dbrenner
Initial revision

-----------------------------------------------------------------------------*/

#import <appkit/Button.h>
#import <appkit/Window.h>
#import <appkit/Box.h>
#import <appkit/PopUpList.h>
#import <appkit/View.h>
#import <objc/List.h>

#include <string.h>

#import "BoxViewSwitcher.h"

@implementation BoxViewSwitcher

/*---------------------------------------------------------------------------
The methods setControlButton:, setBoxViewWindow:, and setBoxView: set the
outlets controlButton, boxViewWindow, and boxView, respectively.  (Connect
these outlets via Interface Builder.)

In all cases, if the id received is of the proper type, its value is stored;
otherwise, the method just returns.

After storing the id, the setup method is called.  Since you cannot (and
should not) depend on the order in which ids are supplied, setup is called
every time but doesn't do anything until all three outlets are initialized.
-----------------------------------------------------------------------------*/
- setControlButton: anObject
{
   if ([anObject isKindOf:[Button class]]) {
      controlButton = anObject;
      [self setup];
   }
   return self;
}

- setBoxViewWindow: anObject
{
   if ([anObject isKindOf:[Window class]]) {
      boxViewWindow = anObject;
      [self setup];
   }
   return self;
}

- setBoxView: anObject
{
   if ([anObject isKindOf:[Box class]]) {
      boxView = anObject;
      [self setup];
   }
   return self;
}

/*---------------------------------------------------------------------------
This method sets the BoxViewSwitcher's delegate to anObject.

After a switch is made, the message boxViewDidSwitchTo: is sent to the delegate
with the title of the new box view.  This message is informative only; the
return value is ignored.
---------------------------------------------------------------------------*/
- setDelegate: anObject { delegate = anObject; return self;}

/*---------------------------------------------------------------------------
These methods return instance variable information.
---------------------------------------------------------------------------*/
- boxViewList { return boxViewList; }
- popup { return popup; }
- boxView { return boxView; }
- controlButton { return controlButton; }

/*---------------------------------------------------------------------------
This method sets everything up.  In particular it does the following:

Creates a popup list of possible box view titles.  (The titles are from
boxView's title and from the title of every box view in the contentView of
boxViewWindow.)

Populates boxViewList with the ids of the possible box views.  (This allows
easy searching and a provides a storage locate for the views after
boxViewWindow is freed.  [This list includes the original boxView.])

Sets the popup list target/action and connects the popup to controlButton.
(The popup list sends selectBoxView: to BoxViewSwitcher when something is
selected, hence we set it's target and action appropriately.  [These could
possibly be over-ridden if controlButton already has a target and action.
See the discussion under NXAttachPopUpList in the PopUpList specification.])

Frees boxViewWindow after removing the box views from its view hierarchy.
(This reduces memory requirements somewhat.  It is assumed that boxViewWindow
is nothing more than a convient place for you to design and store the
alternate box views.  Once collected into boxViewList, the window is useless
to BoxViewSwitcher.)
-----------------------------------------------------------------------------*/
- setup
{
   int i;
   id views;			/* list of subviews in boxViewWindow */
   id oneView;			/* one view from the above list */
   

   /* ---------- we need all three outlets before we can setup */

   if (! (controlButton && boxViewWindow && boxView))
      return self;


   /* ---------- setup popup list and populate boxViewList */

   popup = [[PopUpList alloc] init];	     /* popup list for controlButton */
   boxViewList = [[List alloc] init];         /* we'll save the box views here */

   [popup addItem:[boxView title]];
   [boxViewList addObject:boxView];

   views = [[boxViewWindow contentView] subviews];

   for (i = [views count]-1; i >= 0; i--) {
      oneView = [views objectAt:i];
      if ([oneView isKindOf:[Box class]]) {
	 [popup addItem:[oneView title]];
	 [boxViewList addObject:oneView];
      }
   }
   
   [[popup setTarget:self] setAction:@selector(selectBoxView:)];
   

   /* ---------- setup controlButton */

   [controlButton setTitle:[boxView title]];
   NXAttachPopUpList (controlButton, popup);    
   

   /* ---------- dettach box views from boxViewWindow and free the window */

   views = [[boxViewWindow contentView] subviews];

   for (i = [views count]-1; i >= 0; i--)
      [[views objectAt:i] removeFromSuperview];
      
   [boxViewWindow free];
   boxViewWindow = nil;		/* to prevent problems */
   
   return self;
}

/*---------------------------------------------------------------------------
This is the primary interface for controls that have titles.  (Anything that
uses ButtonCell is a good bet, e.g., Menu, PopUpList.)  This method simply
queries the sender for its title (via [[sender selectedCell] title]) and
uses selectBoxViewTitle: to do the real work.

If BoxViewSwitch did the setup, the method is called by the popup list that
BoxViewSwitch created during the setup method.
-----------------------------------------------------------------------------*/
- selectBoxView: sender
{
   [self selectBoxViewTitle:[[sender selectedCell] title]];
   return self;
}

/*---------------------------------------------------------------------------
This method searches boxViewList for a box view with the same title as the
one passed in.  If a match is found, replaceBoxViewWith: is used to replace
boxView with the found view.

The controlButton title is normally updated by the popup list, but this
method also updates it to handle the case where some other control has made
the selection.  (See the opening comments for more information about using
other controls with BoxViewSwitcher.)

After the switch, the delegate is informed about the change.
-----------------------------------------------------------------------------*/
- selectBoxViewTitle:(const char *) title
{
   int i;
   id thisView;
   
   if (! boxViewList || ! title) return self;
   
   for (i = [boxViewList count]-1; i >= 0; i--) {
      thisView = [boxViewList objectAt:i];

      if (strcmp ([thisView title], title) == 0) {

	 [self replaceBoxViewWith:thisView];

	 [controlButton setTitle:[boxView title]];

	 /* notify the delegate about the switch, if it wants to know */

	 if ([delegate respondsTo:@selector(boxViewDidSwitch:)] == YES)
	    [delegate boxViewDidSwitch:self];

	 break;
      }
   }

   return self;
}

/*---------------------------------------------------------------------------
This method replaces boxView with aView, forces aView into the same frame as
boxView, and asks the superview to redisplay.

The frame of aView must be changed because the various view frames are most
likely different.  Keep in mind that the alternate views start out on another
window and have unknown frames.  It is also possible that boxView's window
may have resized.  (It is this latter resizing issue that really matters
since the alternate box view frames could have been forced during setup.)

It is assumed that the current boxView always has the proper frame, hence,
its frame is forced upon aView.

And a final special note for those of you using View's replaceSubview:with:
method.  Don't replace a view with itself.  This is a degenerate case that
caused me a bit of confusion.  The following code, or its equivalent,

   [aSuperView replaceSubview:aView with:aView]

results in aView being removed from aSuperView's hierarchy.  (At least it
happened at the time of this writing.)  Not necessarily what you might want.
-----------------------------------------------------------------------------*/
- replaceBoxViewWith: aView
{
   NXRect r;
   
   if (! boxView || ! aView) return self;

   if (boxView == aView || [aView isKindOf:[View class]] == NO)
      return self;
   
   [boxView getFrame:&r];
   [aView setFrame:&r];
   
   [[boxView superview] replaceSubview:boxView with:aView];
   [[aView superview] display];
   boxView = aView;
   
   return self;
}

/*---------------------------------------------------------------------------
Frees the popup list, all the views in boxViewList, and boxViewList.

The fact that all the views in boxViewList are freed seems to imply that
BoxViewSwitcher "owns" them.  (This could be considered wrong.)
-----------------------------------------------------------------------------*/
- free
{
   [popup free];
   [boxViewList freeObjects];
   [boxViewList free];
   [super free];
   return self;
}

/*---------------------------------------------------------------------------
This method write the BoxViewSwitcher instance to the typed stream.
---------------------------------------------------------------------------*/
- write:(NXTypedStream *) stream
{
   [super write:stream];

   /* ---------- version 1 variables */
   NXWriteTypes (stream, "@@@", controlButton, boxViewWindow, boxView);
   NXWriteTypes (stream, "@@", popup, boxViewList);

   /* ---------- version 2 variables */
   NXWriteObjectReference (stream, delegate);

   return self;
}

/*---------------------------------------------------------------------------
This method reads the BoxViewSwitcher instance from the typed stream.
---------------------------------------------------------------------------*/
- read: (NXTypedStream *) stream
{
   int streamVer;		/* stream class version */
   
   [super read:stream];

   streamVer = NXTypedStreamClassVersion (stream, "BoxViewSwitcher");
   
   /* ---------- version 1 variables */
   NXReadTypes (stream, "@@@", controlButton, boxViewWindow, boxView);
   NXReadTypes (stream, "@@", popup, boxViewList);

   /* ---------- version 2 variables */
   if (streamVer >= 2) NXReadTypes (stream, "@", delegate);
   
   return self;
}

/*---------------------------------------------------------------------------
This class method sets the version number of the User class.  The current
version number is 2; version number 1 did not implement this method.

This method should not be called directly.
---------------------------------------------------------------------------*/
+ initialize
{
   [super initialize];
   [BoxViewSwitcher setVersion:2];
   return self;
}

@end
